-
Notifications
You must be signed in to change notification settings - Fork 319
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimize Venues search using React Concurrent Mode APIs #3040
base: eliang/concurrent-mode
Are you sure you want to change the base?
Optimize Venues search using React Concurrent Mode APIs #3040
Conversation
Codecov Report
@@ Coverage Diff @@
## eliang/concurrent-mode #3040 +/- ##
==========================================================
+ Coverage 57.16% 57.25% +0.08%
==========================================================
Files 257 258 +1
Lines 5286 5297 +11
Branches 1206 1212 +6
==========================================================
+ Hits 3022 3033 +11
Misses 2264 2264
Continue to review full report at Codecov.
|
Deployment preview for |
{isAvailabilityEnabled !== deferredIsAvailabilityEnabled ? ( | ||
<LoadingSpinner className={styles.availabilitySpinner} white={isAvailabilityEnabled} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This causes the spinner to flash on faster browsers. No idea how to fix it, but I think storing isAvailabilityEnabled !== deferredIsAvailabilityEnabled
in a useState
and using our defer
may be able to fix it at the cost of more renders?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, just click the Find free rooms button. The clock icon in the button will disappear for a moment as the spinner flashes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if putting the spinner here is the best idea - the thing that's "loading" is the venue list below, and it's quite easily missed (though the animation helps). Won't putting a spinner there instead work?
Also thinking about https://reactjs.org/docs/concurrent-mode-patterns.html#delaying-a-pending-indicator, but I don't know how to make that work with useDeferredValue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooh, deferring the loading indicator in CSS is brilliant. I'll switch us to useTransition
then, especially since the current condition for determining whether to show the indicator or not is a bit of a hack.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I lied, useTransition
isn't a good fit for our use case. It's back to useDeferredValue
, but with a deferred spinner :D
/> | ||
|
||
<button | ||
className={classnames( | ||
'btn btn-block btn-svg', | ||
styles.availabilityToggle, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This style didn't exist
4506e67
to
156c1e3
Compare
c8201a6
to
07601ec
Compare
Rebased on parent branch, and fixed conflict with |
156c1e3
to
de2dee6
Compare
…ary and deferred values manually
26d33b3
to
ce167bc
Compare
Rebased on parent branch, no diff change |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to need a bit more time reading up the concurrent mode docs to understand what useDeferredValue is doing here
// Source: https://reactjs.org/docs/concurrent-mode-patterns.html#delaying-a-pending-indicator | ||
.deferred { | ||
visibility: hidden; | ||
animation: 0s linear 0.2s forwards makeVisible; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could in theory make this configurable from the component side using CSS variables or even inline styles, but probably YAGNI
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, probably not a good idea to overengineer this right now hahaa
value: string | null; | ||
placeholder?: string; | ||
onChange: (value: string) => void; | ||
onSearch: () => void; | ||
onSearch?: () => void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be useful adding a small comment on the difference between this and onChange
@@ -73,7 +74,7 @@ const ModuleFinderContainer: React.FC = () => ( | |||
/> | |||
|
|||
<LoadingComponent> | |||
<div className={styles.loadingOverlay} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should remove the unused duplicate class here from ModuleFinderContainer.scss
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, it was already removed
] = useState<string>(() => qs.parse(location.search).q || ''); | ||
/** Actual string to search with; deferred update */ | ||
const deferredSearchQuery = searchQuery; // TODO: Redundant now. Use React.useDeferredValue after we adopt concurrent mode | ||
const [searchQuery, setSearchQuery] = useState<string>(() => qs.parse(location.search).q || ''); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/nit
const [searchQuery, setSearchQuery] = useState<string>(() => qs.parse(location.search).q || ''); | |
const [searchQuery, setSearchQuery] = useState(() => qs.parse(location.search).q || ''); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That doesn't work because qs.parse
returns any
. We could do the below type assertion instead, but the current code seems good enough
const [searchQuery, setSearchQuery] = useState<string>(() => qs.parse(location.search).q || ''); | |
const [searchQuery, setSearchQuery] = useState( | |
() => (qs.parse(location.search) as Params).q || '', | |
); |
const handleSearchChange = useCallback((newSearchQuery: string) => { | ||
setSearchQuery(newSearchQuery); | ||
}, []); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't handleSearchChange
exactly the same as setSearchQuery
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yes, I think I forgot to change this back after my brief experiment with useTransition
{size(matchedVenues) === 0 ? ( | ||
renderNoResult() | ||
) : ( | ||
<div className="position-relative"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think without specifying height this will not take up the whole height of the venues list right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ZhangYiJiang I think reading The TL;DR it looks like it's just storing the previous state in a |
TL;DR
Uses React Concurrent Mode APIs (added in #3039) to optimize the Venues components reworked in #3038.
Optimizations in this PR
Uses
useDeferredValue
to allow the search box to be immediately updated in a sync render before a second, longer concurrent render is performed.master
This may sound similar to the DIY deferred update using 2 nested
requestAnimationFrame
calls that we currently have onmaster
. However, that is inferior to this solution because:master
, the app noticeably freezes up once a legacy render starts as it cannot be interrupted. In the gifs above, notice that onmaster
, a bunch of characters appear at once -- this happens because I typed while the app was frozen.This is what typing a single character looks like in the React Scheduling Profiler (that I helped create :D):
TL;DR of how to read the React data (the stuff above the grey bar):
Notice the short sync render on the left (which is in response to the key press), followed by a very long render caused by the deferred value updating. Also notice that
VenueList
takes a long time to render.This is what typing a second character during a render looks like:
Notice that a concurrent render got abandoned.
Uses
useDeferredValue
to allow the availability filter to open instantly. Onmaster
, this just freezes as there were no optimizations for this.Note that we do not also defer
searchOptions
, as I couldn't get it to feel right. It seems better to just let the page freeze while the render happens. (Edit: it's now deferred. See GIFs in this comment below: #3040 (comment))